package com.zxw.auth.config.security;

import com.zxw.auth.exception.WebResponseTranslator;
import com.zxw.auth.handler.CustomAuthenticationEntryPointHandler;
import com.zxw.auth.security.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import javax.sql.DataSource;

/**
 * @author admin
 * @version 1.0
 * @description: 认证
 * @date 2022/5/27 16:24
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private AuthorizationServerTokenServices tokenService;

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;

    @Autowired
    @Qualifier("myClientDetailsService")
    private ClientDetailsService clientService;


    /**
     * 配置认证客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /**
         * 底层会根据client_id查询oauth_client_details表进项校验
         * 可以自定义方法实现ClientDetailsService接口，来自定义校验规则
         * 系统默认提供2中方式：一是redis方式，二是jdbc方式。
         * 底层思想就是根据client_id查询，然后获取到信息进行校验
         */
        clients.withClientDetails(clientService);
    }

    /**
     * 这里定义获取ClientDetailsService方法，并指定该方法的实现为JdbcClientDetailsService，即从数据库中查询
     *
     * @param dataSource
     * @param passwordEncoder
     * @return
     */
    @Bean("myClientDetailsService")
    public ClientDetailsService clientDetailsService(DataSource dataSource, PasswordEncoder passwordEncoder) {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }


    /**
     * 自定义授权服务配置
     * 使用密码模式需要配置
     * 令牌访问端点
     * 要使用refresh_token的话，需要在认证服务器中配置userDetailsService
     * 如果不配置，或者配置错误（比如在SecurityConfig中配置了一个userDetailsService，而在认证服务器中配置了另一个userDetailsService），会报userDetailsService缺失错误或空指针异常)
     * 需要注意的另一个点：refresh_token必须在过期之前调用才能换新的token。
     * 刷新token时，如果access_token，refresh_token均未过期，access_token会是一个新的token,而且过期时间expires延长,refresh_token根据设定的过期时间,没有失效则不发生变化。
     * 刷新token时，如果access_token过期，refresh_token未过期，access_token会是一个新的token,而且过期时间expires延长,refresh_token根据设定的过期时间,没有失效则不发生变化。
     * 刷新token时，如果refresh_token过期，会返回401状态码，{"error":"invalid_token","error_description":"Invalid refresh token (expired): 7f0cf53a-8b89-4a1f-b5ae-09b7e48dc888"}
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenService)
                //校验用户名密码的入口
                .userDetailsService(userDetailsServiceImpl)
                //指定认证请求方式为post
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                //指定账号密码错误处理
                .exceptionTranslator(new WebResponseTranslator());

    }

    /**
     * 自定义授权令牌端点的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        /**
         * INVALID_CLIENT，TOKEN_NOT_FOUND异常时自定义返回值
         */
        CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler = new CustomAuthenticationEntryPointHandler();
        ClientCredentialsTokenEndpointFilter filter = new ClientCredentialsTokenEndpointFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationEntryPoint(customAuthenticationEntryPointHandler);
        filter.afterPropertiesSet();
        security.addTokenEndpointAuthenticationFilter(filter);

        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Bean
    public TokenEnhancer customerTokenEnhancer(){
        return new JwtTokenEnhancer();
    }

}
